#include <string.h>
#include <stdio.h>
#include <math.h>
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_http_server.h"
#include "driver/ledc.h"
#include "cJSON.h"
#include "lwip/inet.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"

static const char *TAG = "WS_PWM_STA";

/*=================== 用户配置 ===================*/
#define STA_SSID "mitang"
#define STA_PASS "joson_Mt_7410"

/* PWM 路数（建议 ≤4 以保证每路独立频率） */
#define PWM_SLOTS 4

/* 默认引脚（网页可改） */
static int default_pins[16] = {25, 26, 27, 14, 4, 2, 5, 12, 13, 15, 16, 17, 18, 19, 21, 22};
/*================================================*/

/*=================== PWM模式定义 ===================*/
typedef enum {
    PWM_MODE_STANDARD = 0, /* 标准PWM - 电机/LED调光 */
    PWM_MODE_RC_SERVO,     /* RC舵机(50Hz, 5–10%=1–2ms) */
    PWM_MODE_BLDC_MOTOR,   /* ESC油门(帧率50–400Hz, 1000–2000us) */
    PWM_MODE_AUDIO,        /* 音频PWM */
    PWM_MODE_POWER_SUPPLY, /* 电源管理 */
    PWM_MODE_CUSTOM        /* 自定义 */
} pwm_mode_t;

typedef struct {
    pwm_mode_t mode;
    const char *name;
    uint32_t min_freq, max_freq, default_freq;
    float min_duty, max_duty, default_duty;
    uint8_t min_bits, max_bits;
    const char *description;
} pwm_mode_config_t;

static const pwm_mode_config_t pwm_modes[] = {
    {PWM_MODE_STANDARD, "标准PWM", 100, 150000, 1000, 0.0, 100.0, 50.0, 1, 15, "通用PWM（占空=比例）"},
    {PWM_MODE_RC_SERVO, "RC舵机模式", 50, 50, 50, 5.0, 10.0, 7.5, 12, 15, "50Hz；5–10% = 1–2ms"},
    {PWM_MODE_BLDC_MOTOR, "ESC油门", 50, 400, 400, 0.0, 100.0, 0.0, 12, 15, "0–100% → 1000–2000us；50/100/400Hz"},
    {PWM_MODE_AUDIO, "音频PWM", 20000, 100000, 44100, 0.0, 100.0, 50.0, 8, 15, "高频音频PWM"},
    {PWM_MODE_POWER_SUPPLY, "电源管理", 10000, 500000, 100000, 0.0, 100.0, 50.0, 8, 12, "DC-DC 等"},
    {PWM_MODE_CUSTOM, "自定义模式", 1, 1000000, 1000, 0.0, 100.0, 50.0, 1, 15, "完全自定义"},
};
#define NUM_PWM_MODES (sizeof(pwm_modes) / sizeof(pwm_modes[0]))

/*=================== ESC 扩展配置 ===================*/
typedef struct {
    uint16_t esc_min_us;  /* 默认 1000 */
    uint16_t esc_max_us;  /* 默认 2000 */
    bool     esc_reverse; /* 反向油门 */
    uint16_t esc_ramp_ps; /* 限坡，单位：百分比/秒（0=关闭） */
} esc_cfg_t;

/*=================== PWM 槽位结构 ===================*/
typedef struct {
    int pin;
    ledc_timer_t   timer;
    ledc_channel_t channel;
    ledc_mode_t    speed_mode;
    pwm_mode_t     mode;

    uint32_t freq_hz;
    uint8_t  res_bits;

    float duty_pct;        /* 立即生效的占空(或油门) */
    float target_duty_pct; /* 目标占空(或油门) - 用于 ESC 限坡 */
    bool  attached;

    esc_cfg_t esc;
} pwm_slot_t;

static pwm_slot_t g_slots[16];
static uint8_t    g_active_slots = 1; // 初始只有1个激活的槽位
static bool       g_loaded_from_nvs = false;

/*=================== Wi-Fi / HTTPD 句柄 ===================*/
static httpd_handle_t g_httpd = NULL;
static esp_netif_t   *s_sta_netif = NULL;
static EventGroupHandle_t s_wifi_event_group;
#define WIFI_CONNECTED_BIT BIT0

/*=================== 字符串宏 ===================*/
#define STR_HELPER(x) #x
#define STR(x) STR_HELPER(x)

/*=================== 前端 HTML（“回报区”与“仅手动保存”） ===================*/
static const char index_html[] =
"<!DOCTYPE html>"
"<html lang='zh-CN'>"
"<head>"
"<meta charset='UTF-8'>"
"<meta name='viewport' content='width=device-width, initial-scale=1'>"
"<title>ESP32 多模式PWM控制器</title>"
"<style>"
"*{margin:0;padding:0;box-sizing:border-box}"
"body{font-family:'Segoe UI','Microsoft YaHei',sans-serif;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);min-height:100vh;padding:20px;color:#333}"
".container{max-width:1200px;margin:0 auto}"
".header{background:#fff;padding:25px 30px;border-radius:16px;box-shadow:0 8px 32px rgba(0,0,0,.1);margin-bottom:20px;text-align:center;border-left:5px solid #667eea}"
".header h1{font-size:28px;margin-bottom:8px;color:#2c3e50;font-weight:600}"
".subtitle{color:#7f8c8d;margin-bottom:15px;font-size:15px}"
".status-badge{display:inline-flex;align-items:center;background:#f8f9fa;padding:8px 16px;border-radius:20px;font-size:14px;font-weight:500;border:1px solid #e9ecef}"
".status-dot{width:8px;height:8px;border-radius:50%;margin-right:8px;background:#e74c3c;animation:pulse 2s infinite}"
".status-dot.connected{background:#27ae60;animation:none}"
"@keyframes pulse{0%{opacity:1}50%{opacity:.5}100%{opacity:1}}"
".card{background:#fff;padding:20px;border-radius:16px;box-shadow:0 8px 32px rgba(0,0,0,.1);margin-bottom:15px;transition:all .3s ease;border:1px solid #f1f3f4;display:flex;flex-wrap:wrap;gap:15px;align-items:center}"
".card:hover{transform:translateY(-2px);box-shadow:0 12px 40px rgba(0,0,0,.15)}"
".card-header{display:flex;align-items:center;margin-right:20px;min-width:120px}"
".card-title{font-size:16px;font-weight:600;color:#2c3e50;display:flex;align-items:center}"
".channel-badge{background:#3498db;color:#fff;padding:4px 8px;border-radius:8px;font-size:11px;margin-left:8px;font-weight:500}"
".mode-badge{background:#9b59b6;color:#fff;padding:4px 8px;border-radius:8px;font-size:11px;margin-left:6px;font-weight:500}"
".control-group{display:flex;align-items:center;gap:10px;margin-bottom:0}"
".control-label{font-weight:500;color:#5a6c7d;font-size:13px;min-width:60px}"
".input-with-unit{display:flex;align-items:center;gap:6px}"
".number-input{width:80px;padding:8px 10px;border:2px solid #e9ecef;border-radius:8px;font-size:12px;transition:all .3s ease;background:#fafbfc}"
".number-input:focus{outline:none;border-color:#3498db;background:#fff;box-shadow:0 0 0 3px rgba(52,152,219,.1)}"
".unit{color:#7f8c8d;font-size:12px;font-weight:500;min-width:25px}"
".slider-container{width:120px}"
".slider{width:100%;height:4px;border-radius:2px;background:#e9ecef;outline:none;-webkit-appearance:none}"
".slider::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;border-radius:50%;background:#3498db;cursor:pointer;border:2px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,.2);transition:all .2s ease}"
".slider::-webkit-slider-thumb:hover{transform:scale(1.1);background:#2980b9}"
".btn-group{display:flex;gap:8px;margin-left:auto}"
".btn{padding:8px 12px;border:none;border-radius:8px;font-size:12px;font-weight:500;cursor:pointer;transition:all .3s ease;display:inline-flex;align-items:center;justify-content:center;gap:4px}"
".btn-primary{background:#3498db;color:#fff}.btn-primary:hover{background:#2980b9;transform:translateY(-1px);box-shadow:0 4px 12px rgba(52,152,219,.3)}"
".btn-success{background:#27ae60;color:#fff}.btn-success:hover{background:#219a52;transform:translateY(-1px);box-shadow:0 4px 12px rgba(39,174,96,.3)}"
".btn-warning{background:#f39c12;color:#fff}.btn-warning:hover{background:#e67e22;transform:translateY(-1px)}"
".btn-danger{background:#e74c3c;color:#fff}.btn-danger:hover{background:#c0392b;transform:translateY(-1px)}"
".channels-container{display:flex;flex-direction:column;gap:15px}"
".pin-selector,.mode-selector{width:100px;padding:8px 10px;border:2px solid #e9ecef;border-radius:8px;font-size:12px;background:#fafbfc;transition:all .3s ease}"
".pin-selector:focus,.mode-selector:focus{outline:none;border-color:#3498db;background:#fff;box-shadow:0 0 0 3px rgba(52,152,219,.1)}"
".add-card{background:#fff;border:2px dashed #e9ecef;border-radius:16px;padding:18px;display:flex;align-items:center;gap:12px;cursor:pointer;transition:.2s}"
".add-card:hover{background:#f8f9fa}"
".add-icon{width:28px;height:28px;border-radius:8px;background:#3498db;color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700}"
".footer{color:#7f8c8d;font-size:12px;margin-left:auto}"
"#channelsContainer .card [id^='escPanel']{background:#fafbfc;border:1px solid #e9ecef;border-radius:10px;padding:8px 10px;margin:4px 0}"
"</style>"
"</head>"
"<body>"
"<div class='container'>"
"  <div class='header'>"
"    <h1>🎛️ ESP32 多模式PWM控制器</h1>"
"    <div class='subtitle'>单行布局 | 动态通道管理 | 配置<strong>手动保存</strong></div>"
"    <div class='status-badge'><div class='status-dot' id='statusDot'></div><span id='statusText'>连接中...</span></div>"
"  </div>"
"  <div class='channels-container' id='channelsContainer'></div>"
"  <div class='add-card' onclick='addChannel()'>"
"    <div class='add-icon'>+</div>"
"    <h3>添加PWM通道</h3>"
"    <p>点击添加新的PWM控制通道（最多" STR(PWM_SLOTS) "个）</p>"
"  </div>"
"  <div class='card'>"
"    <div class='btn-group'>"
"      <button class='btn btn-success' onclick='requestState()'>🔄 刷新状态</button>"
"      <button class='btn btn-warning' onclick='resetAllToDefault()'>🧹 重置所有</button>"
"      <button class='btn btn-primary' onclick='saveConfig()'>💾 保存配置</button>"
"      <button class='btn btn-danger' onclick='clearConfig()'>🗑️ 清除配置</button>"
"    </div>"
"    <div class='footer'>💡 仅在点击「保存配置」后才会写入NVS并在重启后恢复。ESC模式输出为RC脉宽(1000–2000µs)</div>"
"  </div>"
"</div>"
"<script>"
"const WS_URL='ws://'+location.host+'/ws';"
"let ws,isConnected=false;"
"let currentPins={},currentModes={},activeChannels=1;"
"let uiBuiltForChannels=0;"
"const pwmModes=["
" {id:0,name:'标准PWM',minFreq:100,maxFreq:150000,defaultFreq:1000,minDuty:0,maxDuty:100,defaultDuty:50,desc:'通用PWM（占空=比例）'},"
" {id:1,name:'RC舵机模式',minFreq:50,maxFreq:50,defaultFreq:50,minDuty:5,maxDuty:10,defaultDuty:7.5,desc:'50 Hz，5–10%=1–2 ms'},"
" {id:2,name:'ESC油门(伺服脉冲)',minFreq:50,maxFreq:400,defaultFreq:400,minDuty:0,maxDuty:100,defaultDuty:0,desc:'0–100%→1000–2000µs；50/100/400 Hz'},"
" {id:3,name:'音频PWM',minFreq:20000,maxFreq:100000,defaultFreq:44100,minDuty:0,maxDuty:100,defaultDuty:50,desc:'高频音频'},"
" {id:4,name:'电源管理',minFreq:10000,maxFreq:500000,defaultFreq:100000,minDuty:0,maxDuty:100,defaultDuty:50,desc:'DC-DC 等'},"
" {id:5,name:'自定义模式',minFreq:1,maxFreq:1000000,defaultFreq:1000,minDuty:0,maxDuty:100,defaultDuty:50,desc:'完全自定义'}"
"];"
"const defaultPins=[25,26,27,14,4,2,5,12,13,15,16,17,18,19,21,22];"
"const escCfg=Array(" STR(PWM_SLOTS) ").fill(0).map(_=>({min:1000,max:2000,rev:false,ramp:0}));"
"function send(d){if(ws&&ws.readyState===WebSocket.OPEN){ws.send(JSON.stringify(d))}}"

"function createChannelCard(i){const c=document.createElement('div');c.className='card';c.id='card'+i;c.innerHTML=`"
"  <div class='card-header'><div class='card-title'>📍 PWM ${i+1}<span class='channel-badge' id='badge${i}'>--bit</span><span class='mode-badge' id='modeBadge${i}'>标准PWM</span></div></div>"
"  <div class='control-group'><label class='control-label'>模式</label><select class='mode-selector' id='mode${i}' onchange='updateMode(${i},this.value)'></select></div>"
"  <div class='control-group'><label class='control-label'>引脚</label><select class='pin-selector' id='pin${i}' onchange='updatePin(${i},this.value)'></select></div>"
"  <div class='control-group'>"
"    <label class='control-label'>频率</label>"
"    <div class='input-with-unit'><input type='number' class='number-input' id='freq${i}' oninput='updateFrequency(${i},this.value)'><span class='unit'>Hz</span></div>"
"    <div class='slider-container'><input type='range' class='slider' id='freqSlider${i}' oninput='updateFrequency(${i},this.value)'></div>"
"  </div>"
"  <div class='control-group'>"
"    <label class='control-label'>占空比</label>"
"    <div class='input-with-unit'><input type='number' class='number-input' id='duty${i}' step='0.1' oninput='updateDuty(${i},this.value)'><span class='unit'>%</span></div>"
"    <div class='slider-container'><input type='range' class='slider' id='dutySlider${i}' step='0.1' oninput='updateDuty(${i},this.value)'></div>"
"    <div id='escPulse${i}' style='font-size:12px;color:#5a6c7d;display:none;margin-left:8px;'>脉宽(本地估算): -- µs</div>"
"  </div>"
"  <div id='escPanel${i}' style='display:none;align-items:center;gap:10px;flex-wrap:wrap'>"
"    <div class='control-group'><label class='control-label'>最小</label><div class='input-with-unit'><input type='number' class='number-input' id='escMin${i}' min='500' max='2000' oninput='updateEscCfg(${i})'><span class='unit'>µs</span></div></div>"
"    <div class='control-group'><label class='control-label'>最大</label><div class='input-with-unit'><input type='number' class='number-input' id='escMax${i}' min='1500' max='2500' oninput='updateEscCfg(${i})'><span class='unit'>µs</span></div></div>"
"    "
"    <div class='control-group'><label class='control-label'>限坡</label><div class='input-with-unit'><input type='number' class='number-input' id='escRamp${i}' min='0' max='100' oninput='updateEscCfg(${i})'><span class='unit'>%/s</span></div></div>"
"  </div>"
"  <div class='btn-group' id='presetBar${i}'></div>"
"  <div class='reported' style='width:100%;font-size:12px;color:#5a6c7d;border-top:1px dashed #e9ecef;padding-top:6px'>"
"    <strong>设备回报</strong>："
"    引脚 GPIO <span id='repPin${i}'>--</span> | "
"    模式 <span id='repMode${i}'>--</span> | "
"    频率 <span id='repFreq${i}'>--</span> Hz | "
"    占空 <span id='repDuty${i}'>--</span>% | "
"    分辨率 <span id='repBits${i}'>--</span> bit "
"    <span id='repPulseWrap${i}' style='display:none'> | 脉宽 <span id='repPulse${i}'>--</span> µs</span>"
"  </div>"
"`;return c;}"

"function escPulseUsLocal(i){const duty=parseFloat(document.getElementById('duty'+i)?.value||0);const min=escCfg[i]?.min??1000;const max=escCfg[i]?.max??2000;const rev=!!(escCfg[i]?.rev);const span=Math.max(100,max-min);const thr=rev?(100-duty):duty;const us=Math.round(min+(thr/100)*span);return Math.max(500,Math.min(2500,us));}"
"function refreshEscUI(i){const isEsc=(parseInt(currentModes[i]??0)===2);const panel=document.getElementById('escPanel'+i);const localPulse=document.getElementById('escPulse'+i);if(panel)panel.style.display=isEsc?'flex':'none';if(localPulse){localPulse.style.display=isEsc?'block':'none';if(isEsc)localPulse.textContent='脉宽(本地估算): '+escPulseUsLocal(i)+' µs';}if(isEsc&&escCfg[i]){const{min,max,rev,ramp}=escCfg[i];const eMin=document.getElementById('escMin'+i);const eMax=document.getElementById('escMax'+i);const eRev=document.getElementById('escRev'+i);const eRamp=document.getElementById('escRamp'+i);if(eMin)eMin.value=min??1000; if(eMax)eMax.value=max??2000; if(eRev)eRev.checked=!!rev; if(eRamp)eRamp.value=ramp??0;}}"
"function updateEscCfg(i){const eMin=parseInt(document.getElementById('escMin'+i)?.value||1000);const eMax=parseInt(document.getElementById('escMax'+i)?.value||2000);const eRev=!!document.getElementById('escRev'+i)?.checked;const eRamp=parseInt(document.getElementById('escRamp'+i)?.value||0);escCfg[i]={min:eMin,max:eMax,rev:eRev,ramp:eRamp};clearTimeout(window['escCfgTimeout'+i]);window['escCfgTimeout'+i]=setTimeout(()=>{send({cmd:'esc_cfg',idx:i,escMin:eMin,escMax:eMax,reverse:eRev?1:0,ramp:eRamp});const localPulse=document.getElementById('escPulse'+i);if(localPulse)localPulse.textContent='脉宽(本地估算): '+escPulseUsLocal(i)+' µs';},150);}"

"function renderPresetButtons(i){const wrap=document.getElementById('presetBar'+i);if(!wrap)return;const mode=parseInt(currentModes[i]??0);let html='';if(mode===0){html+=`<button class='btn btn-primary' onclick='applyPresetSmart(${i},\"std_1k\")'>1k/50%</button>`;html+=`<button class='btn btn-primary' onclick='applyPresetSmart(${i},\"std_8k\")'>8k/50%</button>`;}else if(mode===1){html+=`<button class='btn btn-primary' onclick='applyPresetSmart(${i},\"rc_min\")'>5%</button>`;html+=`<button class='btn btn-primary' onclick='applyPresetSmart(${i},\"rc_mid\")'>7.5%</button>`;html+=`<button class='btn btn-primary' onclick='applyPresetSmart(${i},\"rc_max\")'>10%</button>`;}else if(mode===2){html+=`<button class='btn btn-primary' onclick='applyPresetSmart(${i},\"esc_min\")'>1000µs(0%)</button>`;html+=`<button class='btn btn-primary' onclick='applyPresetSmart(${i},\"esc_mid\")'>1500µs(50%)</button>`;html+=`<button class='btn btn-primary' onclick='applyPresetSmart(${i},\"esc_max\")'>2000µs(100%)</button>`;html+=`<button class='btn btn-danger' onclick='escEmergencyStop(${i})'>⛔ 急停</button>`;}else if(mode===3){html+=`<button class='btn btn-primary' onclick='applyPresetSmart(${i},\"aud_44k1\")'>44.1k/50%</button>`;html+=`<button class='btn btn-primary' onclick='applyPresetSmart(${i},\"aud_48k\")'>48k/50%</button>`;}else if(mode===4){html+=`<button class='btn btn-primary' onclick='applyPresetSmart(${i},\"psu_100k\")'>100k/50%</button>`;html+=`<button class='btn btn-primary' onclick='applyPresetSmart(${i},\"psu_300k\")'>300k/50%</button>`;}else{html+=`<button class='btn btn-primary' onclick='applyPresetSmart(${i},\"custom_1k\")'>1k/50%</button>`;}html+=`<button class='btn btn-warning' onclick='removeChannel(${i})' ${i===0?'disabled':''}>删除</button>`;wrap.innerHTML=html;}"
"function applyPresetSmart(i,key){const mode=parseInt(currentModes[i]??0);let f=1000,d=50;if(mode===0){if(key==='std_1k'){f=1000;d=50;}if(key==='std_8k'){f=8000;d=50;}}else if(mode===1){f=50;if(key==='rc_min'){d=5;}if(key==='rc_mid'){d=7.5;}if(key==='rc_max'){d=10;}}else if(mode===2){f=400;if(key==='esc_min'){d=0;}if(key==='esc_mid'){d=50;}if(key==='esc_max'){d=100;}}else if(mode===3){if(key==='aud_44k1'){f=44100;d=50;}if(key==='aud_48k'){f=48000;d=50;}}else if(mode===4){if(key==='psu_100k'){f=100000;d=50;}if(key==='psu_300k'){f=300000;d=50;}}else{if(key==='custom_1k'){f=1000;d=50;}}document.getElementById('freq'+i).value=f;document.getElementById('freqSlider'+i).value=f;document.getElementById('duty'+i).value=d;document.getElementById('dutySlider'+i).value=d;updateFrequency(i,f);updateDuty(i,d);}"
"function escEmergencyStop(i){document.getElementById('duty'+i).value=0;document.getElementById('dutySlider'+i).value=0;refreshEscUI(i);send({cmd:'esc_kill',idx:i});}"

"function updateModeSelectors(){for(let i=0;i<activeChannels;i++){const s=document.getElementById('mode'+i);if(!s)continue;const cur=(currentModes[i]??0);s.innerHTML='';pwmModes.forEach(m=>{const o=document.createElement('option');o.value=m.id;o.textContent=m.name;if(m.id===cur)o.selected=true;s.appendChild(o)});updateModeUI(i,cur);}}"
"function updatePinSelectors(){const pins=defaultPins;for(let i=0;i<activeChannels;i++){const s=document.getElementById('pin'+i);if(!s)continue;const cur=(currentPins[i]??pins[i]);s.innerHTML='';pins.forEach(p=>{const o=document.createElement('option');o.value=p;o.textContent='GPIO '+p;let occ=false;for(let j=0;j<activeChannels;j++){if(j!==i&&currentPins[j]===p){occ=true;break}}if(occ){o.disabled=true;o.textContent+=' (占用)'}if(p===cur)o.selected=true;s.appendChild(o)})}}"
"function updateModeUI(i,modeId){const m=pwmModes.find(x=>x.id==modeId)||pwmModes[0];const fS=document.getElementById('freqSlider'+i),dS=document.getElementById('dutySlider'+i),badge=document.getElementById('modeBadge'+i);if(fS){fS.min=m.minFreq;fS.max=m.maxFreq}if(dS){dS.min=m.minDuty;dS.max=m.maxDuty}if(badge)badge.textContent=m.name;refreshEscUI(i);renderPresetButtons(i);}"
"function initUI(){const c=document.getElementById('channelsContainer');c.innerHTML='';for(let i=0;i<activeChannels;i++){const card=createChannelCard(i);c.appendChild(card);renderPresetButtons(i);/* 控件设置本地默认 */const modeId=(currentModes[i]??0);const m=pwmModes.find(x=>x.id==modeId)||pwmModes[0];const f=m.defaultFreq,d=m.defaultDuty;const freqInp=document.getElementById('freq'+i);const freqSld=document.getElementById('freqSlider'+i);const dutyInp=document.getElementById('duty'+i);const dutySld=document.getElementById('dutySlider'+i);if(freqInp) freqInp.value=f; if(freqSld){freqSld.value=f;freqSld.min=m.minFreq;freqSld.max=m.maxFreq;} if(dutyInp) dutyInp.value=d; if(dutySld){dutySld.value=d;dutySld.min=m.minDuty;dutySld.max=m.maxDuty;} if(currentPins[i]==null) currentPins[i]=defaultPins[i];} updateModeSelectors();updatePinSelectors();uiBuiltForChannels=activeChannels;}"
"function ensureUIBuilt(ch){if(ch!==uiBuiltForChannels){activeChannels=ch;initUI();}}"
"function addChannel(){if(activeChannels<" STR(PWM_SLOTS) "){activeChannels++;initUI();send({cmd:'add_channel',count:activeChannels})}else{alert('已达到最大通道数('+" STR(PWM_SLOTS) "+')')}}"
"function removeChannel(i){if(i>0&&activeChannels>1){if(confirm('确定要删除PWM通道 '+(i+1)+' 吗？')){activeChannels--;send({cmd:'remove_channel',idx:i});initUI();}}else if(i===0){alert('第一个通道不能删除')}}"

"function updateMode(i,val){currentModes[i]=parseInt(val);updateModeUI(i,val);send({cmd:'mode',idx:i,mode:parseInt(val)})}"
"function updatePin(i,p){const n=parseInt(p);currentPins[i]=n;updatePinSelectors();send({cmd:'pin',idx:i,pin:n})}"
"function updateFrequency(i,val){const f=parseInt(val);document.getElementById('freq'+i).value=f;clearTimeout(window['freqTimeout'+i]);window['freqTimeout'+i]=setTimeout(()=>{send({cmd:'set',idx:i,freq:f})},250);refreshEscUI(i);}"
"function updateDuty(i,val){let d=parseFloat(val);document.getElementById('duty'+i).value=d;clearTimeout(window['dutyTimeout'+i]);window['dutyTimeout'+i]=setTimeout(()=>{send({cmd:'set',idx:i,duty:d})},200);refreshEscUI(i);}"
"function applyPreset(i,f,d){document.getElementById('freq'+i).value=f;document.getElementById('freqSlider'+i).value=f;document.getElementById('duty'+i).value=d;document.getElementById('dutySlider'+i).value=d;updateFrequency(i,f);updateDuty(i,d)}"
"function resetAllToDefault(){for(let i=0;i<activeChannels;i++){currentModes[i]=0;applyPreset(i,1000,50)}updateModeSelectors()}"
"function saveConfig(){send({cmd:'save_config'})}"
"function clearConfig(){if(confirm('确定要清除所有配置吗？')){send({cmd:'clear_config'})}}"
"function requestState(){send({cmd:'get'})}"

"function applyState(s){const serverCh = s.activeSlots||1;ensureUIBuilt(serverCh);for(let i=0;i<serverCh;i++){const ch=s.slots[i];if(!ch) continue;/* 设备回报区域 */const modeNames=['标准PWM','RC舵机模式','ESC油门','音频PWM','电源管理','自定义'];const repPin=document.getElementById('repPin'+i);const repMode=document.getElementById('repMode'+i);const repFreq=document.getElementById('repFreq'+i);const repDuty=document.getElementById('repDuty'+i);const repBits=document.getElementById('repBits'+i);const repPulse=document.getElementById('repPulse'+i);const repPulseWrap=document.getElementById('repPulseWrap'+i);if(repPin)  repPin.textContent=ch.pin; if(repMode) repMode.textContent=modeNames[ch.mode]||( '模式'+ch.mode); if(repFreq) repFreq.textContent=ch.freq; if(repDuty) repDuty.textContent=(Math.round(ch.duty*10)/10).toFixed(1); if(repBits) repBits.textContent=ch.resBits; if(ch.mode===2){const escMin=(ch.esc&&typeof ch.esc.escMin==='number')?ch.esc.escMin:1000;const escMax=(ch.esc&&typeof ch.esc.escMax==='number')?ch.esc.escMax:2000;const reverse=!!(ch.esc&&ch.esc.reverse);const span=Math.max(100,escMax-escMin);const thr=reverse?(100-ch.duty):ch.duty;const us=Math.max(500,Math.min(2500,Math.round(escMin+(thr/100)*span)));if(repPulse) repPulse.textContent=us; if(repPulseWrap) repPulseWrap.style.display='inline';}else{if(repPulseWrap) repPulseWrap.style.display='none';}/* 徽章可用于提示设备实际参数 */const modeBadge=document.getElementById('modeBadge'+i);const bitBadge=document.getElementById('badge'+i);if(modeBadge) modeBadge.textContent=modeNames[ch.mode]||'模式';if(bitBadge)  bitBadge.textContent=ch.resBits+'位'+(ch.attached?'':' (未绑定)');}/* 不回写任何输入控件，不更新 currentPins/modes */for(let i=0;i<serverCh;i++) refreshEscUI(i);}"
"function connectWebSocket(){ws=new WebSocket(WS_URL);ws.onopen=()=>{isConnected=true;document.getElementById('statusDot').className='status-dot connected';document.getElementById('statusText').textContent='已连接';requestState()};ws.onclose=()=>{isConnected=false;document.getElementById('statusDot').className='status-dot';document.getElementById('statusText').textContent='断开，重连中...';setTimeout(connectWebSocket,1500)};ws.onerror=()=>{document.getElementById('statusText').textContent='连接错误'};ws.onmessage=(e)=>{try{const m=JSON.parse(e.data);if(m.type==='state')applyState(m);else if(m.type==='config_saved')alert('配置已保存！');else if(m.type==='config_cleared'){alert('配置已清除！');location.reload();}}catch(err){console.error('解析消息错误:',err)}}}"
"document.addEventListener('DOMContentLoaded',()=>{initUI();connectWebSocket()});"
"</script>"
"</body>"
"</html>";

/*=================== NVS 配置存储 ===================*/
#define NVS_NAMESPACE "pwm_config"
#define NVS_KEY_CONFIG "slot_config"

typedef struct {
    uint8_t   active_slots;
    pwm_slot_t slots[PWM_SLOTS];
} pwm_config_t;

static void save_config_to_nvs(void)
{
    nvs_handle_t nvs_handle;
    esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs_handle);
    if (err != ESP_OK) return;

    pwm_config_t config = { .active_slots = g_active_slots };
    memcpy(config.slots, g_slots, sizeof(config.slots)); // 只存前 PWM_SLOTS 个

    err = nvs_set_blob(nvs_handle, NVS_KEY_CONFIG, &config, sizeof(config));
    if (err == ESP_OK) {
        nvs_commit(nvs_handle);
        ESP_LOGI(TAG, "配置已保存到NVS，激活槽位: %d", g_active_slots);
    }
    nvs_close(nvs_handle);
}

static void load_config_from_nvs(void)
{
    g_loaded_from_nvs = false;

    nvs_handle_t nvs_handle;
    esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs_handle);
    if (err != ESP_OK) {
        ESP_LOGI(TAG, "未找到保存的配置，使用默认设置");
        return;
    }

    pwm_config_t config;
    size_t required_size = sizeof(config);
    err = nvs_get_blob(nvs_handle, NVS_KEY_CONFIG, &config, &required_size);

    if (err == ESP_OK && required_size == sizeof(config)) {
        g_active_slots = config.active_slots;
        if (g_active_slots > PWM_SLOTS) g_active_slots = PWM_SLOTS;
        if (g_active_slots < 1)         g_active_slots = 1;

        memset(g_slots, 0, sizeof(g_slots));
        memcpy(g_slots, config.slots, sizeof(config.slots));
        ESP_LOGI(TAG, "从NVS加载配置，激活槽位: %d", g_active_slots);
        g_loaded_from_nvs = true;
    } else {
        ESP_LOGI(TAG, "配置加载失败，使用默认设置");
    }

    nvs_close(nvs_handle);
}

static void clear_config_from_nvs(void)
{
    nvs_handle_t nvs_handle;
    esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs_handle);
    if (err != ESP_OK) return;

    nvs_erase_key(nvs_handle, NVS_KEY_CONFIG);
    nvs_commit(nvs_handle);
    nvs_close(nvs_handle);

    ESP_LOGI(TAG, "NVS配置已清除");
}

/*=================== Wi-Fi（STA） ===================*/
static void wifi_event_handler(void *arg, esp_event_base_t event_base,
                               int32_t event_id, void *event_data)
{
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        ESP_LOGW(TAG, "WiFi disconnected, retry...");
        esp_wifi_connect();
        xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
    } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
        ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&event->ip_info.ip));
        xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
    }
}

static void wifi_init_sta(void)
{
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    s_sta_netif = esp_netif_create_default_wifi_sta();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, NULL));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
                                                        IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL, NULL));

    wifi_config_t wifi_config = {0};
    strncpy((char *)wifi_config.sta.ssid, STA_SSID, sizeof(wifi_config.sta.ssid));
    strncpy((char *)wifi_config.sta.password, STA_PASS, sizeof(wifi_config.sta.password));
    wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;

    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_start());

    s_wifi_event_group = xEventGroupCreate();
    (void)xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT, pdTRUE, pdFALSE, pdMS_TO_TICKS(15000));
}

/*=================== PWM：LEDC 工具 ===================*/
static uint8_t pick_best_res_bits(uint32_t target_hz)
{
    for (int bits = 15; bits >= 1; --bits) {
        double maxF = 80000000.0 / (double)(1u << bits);
        if (maxF >= (double)target_hz) return (uint8_t)bits;
    }
    return 1;
}

static esp_err_t apply_timer_conf(pwm_slot_t *s, uint32_t freq_hz, uint8_t res_bits)
{
    ledc_timer_config_t tc = {
        .speed_mode       = s->speed_mode,
        .duty_resolution  = (ledc_timer_bit_t)res_bits,
        .timer_num        = s->timer,
        .freq_hz          = freq_hz,
        .clk_cfg          = LEDC_AUTO_CLK
    };
    return ledc_timer_config(&tc);
}

static void ensure_channel_attached(pwm_slot_t *s)
{
    if (s->attached) return;
    ledc_channel_config_t cc = {
        .gpio_num   = s->pin,
        .speed_mode = s->speed_mode,
        .channel    = s->channel,
        .timer_sel  = s->timer,
        .intr_type  = LEDC_INTR_DISABLE,
        .duty       = 0,
        .hpoint     = 0
    };
    ESP_ERROR_CHECK(ledc_channel_config(&cc));
    s->attached = true;
}

static void write_duty(pwm_slot_t *s, float duty_pct_now)
{
    const pwm_mode_config_t *mc = &pwm_modes[s->mode];
    float d = duty_pct_now;
    if (d < mc->min_duty) d = mc->min_duty;
    if (d > mc->max_duty) d = mc->max_duty;

    uint32_t period_us = (uint32_t)(1000000UL / (s->freq_hz ? s->freq_hz : 1));
    uint32_t maxDuty   = (1u << s->res_bits) - 1u;
    uint32_t duty_reg  = 0;

    if (s->mode == PWM_MODE_RC_SERVO) {
        uint32_t pulse_us = (uint32_t)((d / 100.0f) * period_us + 0.5f);
        duty_reg = (uint32_t)(((double)pulse_us / (double)period_us) * maxDuty + 0.5);
    } else if (s->mode == PWM_MODE_BLDC_MOTOR) {
        float thr = d;
        if (s->esc.esc_reverse) thr = 100.0f - thr;
        uint32_t span = (s->esc.esc_max_us > s->esc.esc_min_us) ? (s->esc.esc_max_us - s->esc.esc_min_us) : 1000;
        uint32_t pulse_us = s->esc.esc_min_us + (uint32_t)((thr / 100.0f) * span + 0.5f);
        if (pulse_us < s->esc.esc_min_us) pulse_us = s->esc.esc_min_us;
        if (pulse_us > s->esc.esc_max_us) pulse_us = s->esc.esc_max_us;
        duty_reg = (uint32_t)(((double)pulse_us / (double)period_us) * maxDuty + 0.5);
    } else {
        duty_reg = (uint32_t)((d / 100.0f) * maxDuty + 0.5f);
    }

    ESP_ERROR_CHECK(ledc_set_duty(s->speed_mode, s->channel, duty_reg));
    ESP_ERROR_CHECK(ledc_update_duty(s->speed_mode, s->channel));
    s->duty_pct = d;
}

static void set_frequency_keep_effect(pwm_slot_t *s, uint32_t new_freq_hz)
{
    const pwm_mode_config_t *mc = &pwm_modes[s->mode];
    uint32_t f = new_freq_hz;
    if (f < mc->min_freq) f = mc->min_freq;
    if (f > mc->max_freq) f = mc->max_freq;

    uint8_t bits = pick_best_res_bits(f);
    esp_err_t err;
    while ((err = apply_timer_conf(s, f, bits)) != ESP_OK && bits > 1) bits--;
    if (err != ESP_OK) {
        bits = 1;
        ESP_ERROR_CHECK(apply_timer_conf(s, f, bits));
    }

    s->freq_hz = f;
    s->res_bits = bits;

    ensure_channel_attached(s);
    write_duty(s, s->duty_pct);
}

static void reattach_pin(pwm_slot_t *s, int new_pin)
{
    if (s->attached) {
        ledc_stop(s->speed_mode, s->channel, 0);
        s->attached = false;
    }
    s->pin = new_pin;
    ensure_channel_attached(s);
    write_duty(s, s->duty_pct);
}

static void set_pwm_mode(pwm_slot_t *s, pwm_mode_t mode)
{
    if (mode >= NUM_PWM_MODES) mode = PWM_MODE_STANDARD;
    s->mode = mode;

    const pwm_mode_config_t *mc = &pwm_modes[mode];
    set_frequency_keep_effect(s, mc->default_freq);
    s->target_duty_pct = mc->default_duty;
    s->duty_pct        = mc->default_duty;
    write_duty(s, s->duty_pct);
}

/*=================== 槽位初始化 ===================*/
static void init_one_slot(int i, bool attach_and_start)
{
    pwm_slot_t *s = &g_slots[i];
    s->pin        = default_pins[i];
    s->timer      = (ledc_timer_t)i;           // 每路独立定时器
    s->channel    = (ledc_channel_t)i;
    s->speed_mode = LEDC_LOW_SPEED_MODE;
    s->mode       = PWM_MODE_STANDARD;

    s->freq_hz    = pwm_modes[s->mode].default_freq;
    s->res_bits   = pick_best_res_bits(s->freq_hz);
    s->duty_pct   = pwm_modes[s->mode].default_duty;
    s->target_duty_pct = s->duty_pct;
    s->attached   = false;

    s->esc.esc_min_us  = 1000;
    s->esc.esc_max_us  = 2000;
    s->esc.esc_reverse = false;
    s->esc.esc_ramp_ps = 0;

    if (attach_and_start) {
        ESP_ERROR_CHECK(apply_timer_conf(s, s->freq_hz, s->res_bits));
        ensure_channel_attached(s);
        write_duty(s, s->duty_pct);
    }
}

static void init_slots_defaults(void)
{
    for (int i = 0; i < PWM_SLOTS; ++i) {
        init_one_slot(i, i < g_active_slots);
    }
}

/* 用当前 g_slots 内容进行 attach（用于 NVS 恢复） */
static void init_slots_from_current(bool attach_and_start)
{
    for (int i = 0; i < PWM_SLOTS; ++i) {
        pwm_slot_t *s = &g_slots[i];

        if (s->timer > LEDC_TIMER_3) s->timer = (ledc_timer_t)(i % 4);
        if (s->channel > LEDC_CHANNEL_7) s->channel = (ledc_channel_t)(i % 8);
        s->speed_mode = LEDC_LOW_SPEED_MODE;

        if (i < g_active_slots && attach_and_start) {
            if (s->pin <= 0) s->pin = default_pins[i];
            if (s->mode >= NUM_PWM_MODES) s->mode = PWM_MODE_STANDARD;
            if (s->freq_hz == 0) s->freq_hz = pwm_modes[s->mode].default_freq;
            if (s->res_bits == 0) s->res_bits = pick_best_res_bits(s->freq_hz);
            ESP_ERROR_CHECK(apply_timer_conf(s, s->freq_hz, s->res_bits));
            ensure_channel_attached(s);
            write_duty(s, s->duty_pct == 0 ? pwm_modes[s->mode].default_duty : s->duty_pct);
        }
    }
}

/*=================== Web 页面与 WS ===================*/
static esp_err_t root_get_handler(httpd_req_t *req)
{
    httpd_resp_set_type(req, "text/html; charset=utf-8");
    return httpd_resp_send(req, index_html, HTTPD_RESP_USE_STRLEN);
}

static void send_state_one(httpd_req_t *req, int client_fd)
{
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "type", "state");
    cJSON_AddNumberToObject(root, "activeSlots", g_active_slots);

    cJSON *arr = cJSON_CreateArray();
    cJSON_AddItemToObject(root, "slots", arr);

    for (int i = 0; i < g_active_slots; ++i) {
        cJSON *o = cJSON_CreateObject();
        cJSON_AddNumberToObject(o, "idx", i);
        cJSON_AddNumberToObject(o, "pin", g_slots[i].pin);
        cJSON_AddNumberToObject(o, "resBits", g_slots[i].res_bits);
        cJSON_AddNumberToObject(o, "freq", g_slots[i].freq_hz);
        cJSON_AddNumberToObject(o, "duty", g_slots[i].duty_pct);
        cJSON_AddNumberToObject(o, "mode", g_slots[i].mode);
        cJSON_AddBoolToObject(o, "attached", g_slots[i].attached);

        cJSON *esc = cJSON_CreateObject();
        cJSON_AddNumberToObject(esc, "escMin", g_slots[i].esc.esc_min_us);
        cJSON_AddNumberToObject(esc, "escMax", g_slots[i].esc.esc_max_us);
        cJSON_AddBoolToObject(esc, "reverse", g_slots[i].esc.esc_reverse);
        cJSON_AddNumberToObject(esc, "ramp", g_slots[i].esc.esc_ramp_ps);
        cJSON_AddItemToObject(o, "esc", esc);

        cJSON_AddItemToArray(arr, o);
    }

    char *out = cJSON_PrintUnformatted(root);
    cJSON_Delete(root);

    httpd_ws_frame_t ws_pkt = {
        .type = HTTPD_WS_TYPE_TEXT,
        .payload = (uint8_t *)out,
        .len = strlen(out),
    };
    if (client_fd >= 0)
        httpd_ws_send_frame_async(g_httpd, client_fd, &ws_pkt);
    else
        httpd_ws_send_frame(req, &ws_pkt);
    free(out);
}

static void send_config_saved(httpd_req_t *req, int client_fd)
{
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "type", "config_saved");
    char *out = cJSON_PrintUnformatted(root);
    cJSON_Delete(root);

    httpd_ws_frame_t ws_pkt = {.type = HTTPD_WS_TYPE_TEXT, .payload = (uint8_t *)out, .len = strlen(out)};
    if (client_fd >= 0) httpd_ws_send_frame_async(g_httpd, client_fd, &ws_pkt);
    else                httpd_ws_send_frame(req, &ws_pkt);
    free(out);
}

static void send_config_cleared(httpd_req_t *req, int client_fd)
{
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "type", "config_cleared");
    char *out = cJSON_PrintUnformatted(root);
    cJSON_Delete(root);

    httpd_ws_frame_t ws_pkt = {.type = HTTPD_WS_TYPE_TEXT, .payload = (uint8_t *)out, .len = strlen(out)};
    if (client_fd >= 0) httpd_ws_send_frame_async(g_httpd, &ws_pkt, &ws_pkt); // 注意：新版本 API 若有改，用上面的形式
    else                httpd_ws_send_frame(req, &ws_pkt);
    free(out);
}

/*=================== 命令处理（仅“保存”才写NVS；state 不回写控件） ===================*/
static void handle_json_cmd(const char *json, httpd_req_t *req, int client_fd)
{
    cJSON *root = cJSON_Parse(json);
    if (!root) return;

    const cJSON *cmd = cJSON_GetObjectItem(root, "cmd");
    if (cmd && cJSON_IsString(cmd)) {
        if (strcmp(cmd->valuestring, "get") == 0) {
            send_state_one(req, client_fd);

        } else if (strcmp(cmd->valuestring, "set") == 0) {
            cJSON *idx = cJSON_GetObjectItem(root, "idx");
            if (idx && cJSON_IsNumber(idx)) {
                int i = idx->valueint;
                if (i >= 0 && i < g_active_slots) {
                    cJSON *jf = cJSON_GetObjectItem(root, "freq");
                    cJSON *jd = cJSON_GetObjectItem(root, "duty");

                    if (jf && cJSON_IsNumber(jf)) {
                        set_frequency_keep_effect(&g_slots[i], (uint32_t)jf->valuedouble);
                    }
                    if (jd && cJSON_IsNumber(jd)) {
                        float d = (float)jd->valuedouble;
                        g_slots[i].target_duty_pct = d;

                        if (g_slots[i].mode == PWM_MODE_BLDC_MOTOR && g_slots[i].esc.esc_ramp_ps > 0) {
                            // 限坡任务里逐步逼近
                        } else {
                            g_slots[i].duty_pct = d;
                            write_duty(&g_slots[i], g_slots[i].duty_pct);
                        }
                    }
                    send_state_one(req, client_fd);
                    // 不自动保存
                }
            }

        } else if (strcmp(cmd->valuestring, "pin") == 0) {
            cJSON *idx  = cJSON_GetObjectItem(root, "idx");
            cJSON *jpin = cJSON_GetObjectItem(root, "pin");
            if (idx && cJSON_IsNumber(idx) && jpin && cJSON_IsNumber(jpin)) {
                int i = idx->valueint, p = jpin->valueint;
                if (i >= 0 && i < g_active_slots) {
                    reattach_pin(&g_slots[i], p);
                    send_state_one(req, client_fd);
                    // 不自动保存
                }
            }

        } else if (strcmp(cmd->valuestring, "mode") == 0) {
            cJSON *idx   = cJSON_GetObjectItem(root, "idx");
            cJSON *jmode = cJSON_GetObjectItem(root, "mode");
            if (idx && cJSON_IsNumber(idx) && jmode && cJSON_IsNumber(jmode)) {
                int i = idx->valueint;
                pwm_mode_t m = (pwm_mode_t)jmode->valueint;
                if (i >= 0 && i < g_active_slots) {
                    set_pwm_mode(&g_slots[i], m);
                    send_state_one(req, client_fd);
                    // 不自动保存
                }
            }

        } else if (strcmp(cmd->valuestring, "add_channel") == 0) {
            cJSON *jcount = cJSON_GetObjectItem(root, "count");
            if (jcount && cJSON_IsNumber(jcount)) {
                uint8_t new_count = jcount->valueint;
                if (new_count <= PWM_SLOTS && new_count > g_active_slots) {
                    for (int i = g_active_slots; i < new_count; ++i) {
                        init_one_slot(i, true);
                    }
                    g_active_slots = new_count;
                    send_state_one(req, client_fd);
                    // 不自动保存
                }
            }

        } else if (strcmp(cmd->valuestring, "remove_channel") == 0) {
            cJSON *idx = cJSON_GetObjectItem(root, "idx");
            if (idx && cJSON_IsNumber(idx)) {
                int i = idx->valueint;
                if (i > 0 && i < g_active_slots) {
                    if (g_slots[i].attached) {
                        ledc_stop(g_slots[i].speed_mode, g_slots[i].channel, 0);
                        g_slots[i].attached = false;
                    }
                    // 左移压紧
                    for (int k = i; k < g_active_slots - 1; ++k) {
                        g_slots[k] = g_slots[k + 1];
                    }
                    // 清理最后一个槽
                    init_one_slot(g_active_slots - 1, false);
                    g_active_slots--;
                    send_state_one(req, client_fd);
                    // 不自动保存
                }
            }

        } else if (strcmp(cmd->valuestring, "save_config") == 0) {
            save_config_to_nvs();
            send_config_saved(req, client_fd);

        } else if (strcmp(cmd->valuestring, "clear_config") == 0) {
            clear_config_from_nvs();
            g_active_slots = 1;
            init_slots_defaults();
            send_config_cleared(req, client_fd);

        } else if (strcmp(cmd->valuestring, "esc_cfg") == 0) {
            cJSON *idx = cJSON_GetObjectItem(root, "idx");
            if (idx && cJSON_IsNumber(idx)) {
                int i = idx->valueint;
                if (i >= 0 && i < g_active_slots) {
                    cJSON *jmin = cJSON_GetObjectItem(root, "escMin");
                    cJSON *jmax = cJSON_GetObjectItem(root, "escMax");
                    cJSON *jrev = cJSON_GetObjectItem(root, "reverse");
                    cJSON *jrmp = cJSON_GetObjectItem(root, "ramp");

                    if (jmin && cJSON_IsNumber(jmin)) g_slots[i].esc.esc_min_us = (uint16_t)jmin->valuedouble;
                    if (jmax && cJSON_IsNumber(jmax)) g_slots[i].esc.esc_max_us = (uint16_t)jmax->valuedouble;
                    if (jrev && cJSON_IsNumber(jrev)) g_slots[i].esc.esc_reverse = (jrev->valueint != 0);
                    if (jrmp && cJSON_IsNumber(jrmp)) g_slots[i].esc.esc_ramp_ps  = (uint16_t)jrmp->valuedouble;

                    write_duty(&g_slots[i], g_slots[i].duty_pct);
                    send_state_one(req, client_fd);
                    // 不自动保存
                }
            }

        } else if (strcmp(cmd->valuestring, "esc_kill") == 0) {
            cJSON *idx = cJSON_GetObjectItem(root, "idx");
            if (idx && cJSON_IsNumber(idx)) {
                int i = idx->valueint;
                if (i >= 0 && i < g_active_slots) {
                    pwm_slot_t *s = &g_slots[i];
                    if (s->mode == PWM_MODE_BLDC_MOTOR) {
                        s->target_duty_pct = 0.0f;  // 目标0
                        s->duty_pct = 0.0f;        // 立刻0（绕过限坡）
                        write_duty(s, s->duty_pct);
                        if (s->freq_hz != 400) {   // 可选：强制回到 400 Hz
                            set_frequency_keep_effect(s, 400);
                        }
                        send_state_one(req, client_fd);
                        // 不自动保存
                    }
                }
            }
        }
    }
    cJSON_Delete(root);
}

static esp_err_t ws_handler(httpd_req_t *req)
{
    if (req->method == HTTP_GET) {
        ESP_LOGI(TAG, "WS client connected");
        return ESP_OK;
    }
    httpd_ws_frame_t ws_pkt = {0};
    ws_pkt.type = HTTPD_WS_TYPE_TEXT;

    esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0);
    if (ret != ESP_OK) return ret;

    if (ws_pkt.len) {
        ws_pkt.payload = malloc(ws_pkt.len + 1);
        if (!ws_pkt.payload) return ESP_ERR_NO_MEM;
        ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len);
        if (ret != ESP_OK) { free(ws_pkt.payload); return ret; }
        ws_pkt.payload[ws_pkt.len] = 0;
    }

    int client_fd = httpd_req_to_sockfd(req);
    if (ws_pkt.type == HTTPD_WS_TYPE_TEXT && ws_pkt.payload) {
        handle_json_cmd((char *)ws_pkt.payload, req, client_fd);
    }
    if (ws_pkt.payload) free(ws_pkt.payload);
    return ESP_OK;
}

static httpd_handle_t start_webserver(void)
{
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.server_port = 80;
    config.max_uri_handlers = 8;
    config.lru_purge_enable = true;
    config.uri_match_fn = httpd_uri_match_wildcard;

    httpd_handle_t server = NULL;
    if (httpd_start(&server, &config) == ESP_OK) {
        httpd_uri_t root = {.uri = "/", .method = HTTP_GET, .handler = root_get_handler, .user_ctx = NULL};
        httpd_register_uri_handler(server, &root);

        httpd_uri_t ws = { .uri = "/ws", .method = HTTP_GET, .handler = ws_handler, .user_ctx = NULL, .is_websocket = true };
        httpd_register_uri_handler(server, &ws);

        ESP_LOGI(TAG, "HTTP on :80, WS on /ws");
        return server;
    }
    ESP_LOGE(TAG, "Failed starting http server");
    return NULL;
}

/*=================== ESC 限坡任务（20ms tick） ===================*/
static void pwm_ramp_task(void *arg)
{
    const TickType_t tick = pdMS_TO_TICKS(20);
    for (;;) {
        for (int i = 0; i < g_active_slots; i++) {
            pwm_slot_t *s = &g_slots[i];
            if (s->mode == PWM_MODE_BLDC_MOTOR && s->esc.esc_ramp_ps > 0) {
                float step = (s->esc.esc_ramp_ps) * 0.02f; // 每20ms步进
                float tgt = s->target_duty_pct;
                float cur = s->duty_pct;
                if (fabsf(tgt - cur) <= step || step <= 0.001f) {
                    if (cur != tgt) {
                        s->duty_pct = tgt;
                        write_duty(s, s->duty_pct);
                    }
                } else {
                    s->duty_pct = cur + (tgt > cur ? step : -step);
                    write_duty(s, s->duty_pct);
                }
            }
        }
        vTaskDelay(tick);
    }
}

/*=================== app_main ===================*/
void app_main(void)
{
    ESP_ERROR_CHECK(nvs_flash_init());

    // 先加载配置（如果有）
    load_config_from_nvs();

    wifi_init_sta();

    if (g_loaded_from_nvs) {
        init_slots_from_current(true);   // 按 NVS 中的参数 attach & 恢复
    } else {
        g_active_slots = 1;              // 保底
        init_slots_defaults();           // 用默认参数初始化
    }

    g_httpd = start_webserver();

    esp_netif_ip_info_t ip;
    if (s_sta_netif && esp_netif_get_ip_info(s_sta_netif, &ip) == ESP_OK) {
        ESP_LOGI(TAG, "Open: http://" IPSTR, IP2STR(&ip.ip));
    }

    xTaskCreate(pwm_ramp_task, "pwm_ramp", 3072, NULL, 3, NULL);

    ESP_LOGI(TAG, "系统启动完成，激活通道数: %d", g_active_slots);

    // app_main 不返回，避免 FreeRTOS “任务返回导致 abort”
    for (;;) {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}
